#!/usr/bin/env node

/**
 * This script reads and parses all of the source code files, reads the US English
 * translation files, extracts all http/s links and checks for broken links.
 *
 * This script is used in the PR gate to check for broken links.
 *
 */

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const axios = require('axios');

const base = path.resolve(__dirname, '..');
const srcFolder = path.resolve(base, 'shell');
const pkgFolder = path.resolve(base, 'pkg');

// Simple shell colors
const reset = '\x1b[0m';
const cyan = `\x1b[96m`;
const yellow = `\x1b[33m`;
const bold = `\x1b[1m`;

const DOCS_BASE_REGEX = /export const DOCS_BASE = `([^']*v)\${ CURRENT_RANCHER_VERSION }`/;
const VERSION_REGEX = /export const CURRENT_RANCHER_VERSION = '([0-9]\.[0-9]+)';/;

const CATEGORIES = [
  {
    name:  'Rancher Manager Documentation',
    regex: /^https:\/\/.*rancher\.com\/.*/
  },
  {
    name:  'RKE2 Documentation',
    regex: /^https:\/\/.*rke2\.io\/.*/
  },
  {
    name:  'K3S Documentation',
    regex: /^https:\/\/.*k3s\.io\/.*/
  }
];

let docsBaseUrl = '';

// -x flag will cause script to return 0, even if there are errors
let doNotReturnError = false;

// Simple arg parsing
if (process.argv.length > 2) {
  process.argv.shift();
  process.argv.shift();

  process.argv.forEach((arg) => {
    if (arg === '-x') {
      doNotReturnError = true;
    }
  });
}

// Read file to parse out the docs base
const docsBaseFile = fs.readFileSync(path.join(srcFolder, 'config', 'private-label.js'), 'utf8');
const docsBaseFileMatches = docsBaseFile.match(DOCS_BASE_REGEX);

if (docsBaseFileMatches && docsBaseFileMatches.length === 2) {
  docsBaseUrl = docsBaseFileMatches[1];
}

if (docsBaseUrl.length === 0) {
  console.log('Could not parse documentation base URL'); // eslint-disable-line no-console
  process.exit(1);
}

const versionFile = fs.readFileSync(path.join(srcFolder, 'config', 'version.js'), 'utf8');
const versionFileMatches = versionFile.match(VERSION_REGEX);

if (versionFileMatches && versionFileMatches.length === 2) {
  docsBaseUrl = `${ docsBaseUrl }${ versionFileMatches[1] }`;
} else {
  console.log('Could not parse version number from the version file'); // eslint-disable-line no-console
  process.exit(1);
}

function readAndParseTranslations(filePath) {
  const data = fs.readFileSync(filePath, 'utf8');

  try {
    const i18n = yaml.load(fs.readFileSync(filePath), 'utf8');

    return parseTranslations(i18n);
  } catch (e) {
    console.log('Can not read i18n file'); // eslint-disable-line no-console
    console.log(e); // eslint-disable-line no-console
    process.exit(1);
  }
}

function parseTranslations(obj, parent) {
  let res = {};

  Object.keys(obj).forEach((key) => {
    const v = obj[key];
    const pKey = parent ? `${ parent }.${ key }` : key;

    if (v === null) {
      // ignore
    } else if (typeof v === 'object') {
      res = {
        ...res,
        ...parseTranslations(v, pKey)
      };
    } else {
      // Ensure empty strings work
      res[pKey] = v.length === 0 ? '[empty]' : v;
    }
  });

  return res;
}

const LINK_REGEX = /<[aA]\s[^>]*>/g;
const ATTR_REGEX = /(([a-zA-Z]*)=["']([^"']*))/g;

function parseLinks(str) {
  const a = [...str.matchAll(LINK_REGEX)];

  const links = [];

  if (a && a.length) {
    a.forEach((m) => {
      const attrs = [...m[0].matchAll(ATTR_REGEX)];

      attrs.forEach((attr) => {
        if (attr.length === 4 && attr[2] === 'href') {
          const link = attr[3].replace('{docsBase}', docsBaseUrl);

          if (link.startsWith('http')) {
            links.push(link);
          } else if (!(link.startsWith('{') && link.endsWith('}'))) {
            console.log(`${ yellow }${bold}Skipping link: ${ link }${ reset }`); // eslint-disable-line no-console
          }
        }
      });
    });
  }

  return links;
}

function loadI18nFiles(folder) {
  let res = {};

  // Find all of the test files
  fs.readdirSync(folder).forEach((file) => {
    const filePath = path.resolve(folder, file);
    const isFolder = fs.lstatSync(filePath).isDirectory();

    if (isFolder) {
      res = {
        ...res,
        ...loadI18nFiles(filePath)
      };
    } else if (file === 'en-us.yaml') {
      console.log(` ... ${ path.relative(base, filePath) }`); // eslint-disable-line no-console

      const translations = readAndParseTranslations(filePath);

      res = {
        ...res,
        ...translations
      };
    }
  });

  return res;
}

console.log('======================================'); // eslint-disable-line no-console
console.log(`${ cyan }Checking source files for i18n strings${ reset }`); // eslint-disable-line no-console
console.log('======================================'); // eslint-disable-line no-console

console.log(''); // eslint-disable-line no-console

console.log(`Using documentation base URL: ${ cyan }${ docsBaseUrl }${ reset }`); // eslint-disable-line no-console
console.log(''); // eslint-disable-line no-console


console.log('Reading translation files:'); // eslint-disable-line no-console

let i18n = loadI18nFiles(srcFolder);

i18n = { ...i18n, ...loadI18nFiles(pkgFolder) };

console.log(`Read  ${ cyan }${ Object.keys(i18n).length }${ reset } translations`); // eslint-disable-line no-console
console.log(''); // eslint-disable-line no-console

const links = [];

// Parse all of the links from each translation file
Object.keys(i18n).forEach((str) => {
  const link = parseLinks(i18n[str]);

  links.push(...link);
});

console.log(`Discovered ${ cyan }${ links.length }${ reset } links`); // eslint-disable-line no-console
console.log(''); // eslint-disable-line no-console

console.log(`${ cyan }Links:${ reset }`); // eslint-disable-line no-console
console.log(`${ cyan }======${ reset }`); // eslint-disable-line no-console

showByCategory(links, 'Links in', 'Other Links', cyan);

console.log(''); // eslint-disable-line no-console

function showByCategory(linksToShow, prefixLabel, otherLabel, color) {
  const others = [];
  const byCategory = {};

  linksToShow.forEach((link) => {
    let found = false;

    CATEGORIES.forEach((category) => {
      byCategory[category.name] = byCategory[category.name] || [];

      if (!found && category.regex.test(link)) {
        byCategory[category.name].push(link);
        found = true;
      }
    });

    if (!found) {
      others.push(link);
    }
  });

  CATEGORIES.forEach((category) => {
    if (byCategory[category.name]?.length) {
      console.log(`${ color }${ prefixLabel } ${ category.name }${ reset }`); // eslint-disable-line no-console
      byCategory[category.name].forEach((link) => console.log(`  ${ link }`)); // eslint-disable-line no-console
    }
  });

  if (others.length) {
    console.log(`${ color }${ otherLabel }${ reset }`); // eslint-disable-line no-console
    others.forEach((link) => console.log(`  ${ link }`));
  }
}

async function check(links) {
  const brokenLinks = [];

  for (let i = 0; i < links.length; i++) {
    const link = links[i];
    let statusCode;
    let statusMessage;

    try {
      const headers = {
        'User-Agent':      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
        Accept:            'text/html',
        'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
      };

      const r = await axios.get(link, { headers });

      statusCode = r.status;
      statusMessage = r.statusText;
    } catch (e) {
      statusCode = e.response ? e.response.status : e.status;
      statusMessage = e.response ? e.response.statusText : e.statusText;
    }

    if (statusCode !== 200) {
      const sc = `${ statusCode }`.padEnd(5);

      console.log(`  ${ link } : ${ sc } ${ statusMessage || '' }`); // eslint-disable-line no-console

      brokenLinks.push(link);
    }
  }

  console.log(''); // eslint-disable-line no-console

  if (brokenLinks.length === 0) {
    console.log(`${ cyan }${ bold }Links Checked - all links could be fetched okay${ reset }`); // eslint-disable-line no-console
  } else {
    console.log(`${ yellow }${ bold }Found ${ brokenLinks.length } link(s) that could not be fetched${ reset }`); // eslint-disable-line no-console
  }

  console.log(''); // eslint-disable-line no-console

  showByCategory(brokenLinks, 'Broken links in', 'Other Broken links', yellow);

  console.log(''); // eslint-disable-line no-console

  // Return with error code if broken links found
  if (!doNotReturnError && brokenLinks.length > 0) {
    process.exit(1);
  }
}

console.log(`${ cyan }Checking doc links ...${ reset }`); // eslint-disable-line no-console

check(links);
